/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { MediaType } from './MediaType';
import { Charset } from './utils/Charset';
import { util } from '@kit.ArkTS';
import { checkOffsetAndCount } from './utils/Utils';
import { fileIo as fs } from '@kit.CoreFileKit'
import { Sink } from './okio/Sink';
import { IOException } from './Error/error';

export abstract class RequestBody {
    /** Buffer size for file reading operations */
    private static readonly FILE_BUFFER_SIZE = 8192;

    /** Returns the Content-Type header for this body. */
    abstract contentType(): MediaType | null

    /**
     * Returns the number of bytes that will be written to sink in a call to [writeTo],
     * or -1 if that count is unknown.
     */
    // @Throws(IOException::class)
    abstract contentLength(): number | Promise<number>
    /** Writes the content of this request to [sink]. */
    abstract writeTo(sink: Sink): void | Promise<void>

    /**
     * A duplex request body is special in how it is **transmitted** on the network and
     * in the **API contract** between OkHttp and the application.
     *
     * This method returns false unless it is overridden by a subclass.
     *
     * ### Duplex Transmission
     *
     * With regular HTTP calls the request always completes sending before the response may begin
     * receiving. With duplex the request and response may be interleaved! That is, request body bytes
     * may be sent after response headers or body bytes have been received.
     *
     * Though any call may be initiated as a duplex call, only web servers that are specially
     * designed for this nonstandard interaction will use it. As of 2019-01, the only widely-used
     * implementation of this pattern is [gRPC](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md).
     *
     * Because the encoding of interleaved data is not well-defined for HTTP/1, duplex request
     * bodies may only be used with HTTP/2. Calls to HTTP/1 servers will fail before the HTTP request
     * is transmitted. If you cannot ensure that your client and server both support HTTP/2, do not
     * use this feature.
     *
     * ### Duplex APIs
     *
     * With regular request bodies it is not legal to write bytes to the sink passed to
     * [RequestBody.writeTo] after that method returns. For duplex requests bodies that condition is
     * lifted. Such writes occur on an application-provided thread and may occur concurrently with
     * reads of the ResponseBody. For duplex request bodies, [writeTo] should return
     * quickly, possibly by handing off the provided request body to another thread to perform
     * writing.
     */
    isDuplex(): boolean {
        return false
    }

    /**
     * Returns true if this body expects at most one call to [writeTo] and can be transmitted
     * at most once. This is typically used when writing the request body is destructive and it is not
     * possible to recreate the request body after it has been sent.
     *
     * This method returns false unless it is overridden by a subclass.
     *
     * By default OkHttp will attempt to retransmit request bodies when the original request fails
     * due to any of:
     *
     *  * A stale connection. The request was made on a reused connection and that reused connection
     *    has since been closed by the server.
     *  * A client timeout (HTTP 408).
     *  * A authorization challenge (HTTP 401 and 407) that is satisfied by the Authenticator.
     *  * A retryable server failure (HTTP 503 with a `Retry-After: 0` response header).
     *  * A misdirected request (HTTP 421) on a coalesced connection.
     */
    isOneShot(): boolean {
        return false
    }

     /**
     * Returns a new request body that transmits this string. If [contentType] is non-null and lacks
     * a charset, this will use UTF-8.
     */
    // @JvmStatic
    // @JvmName("create")
    static StringToRequestBody(str: string, contentType?: MediaType | null): RequestBody {
        let charset: Charset = Charset.UTF_8;
        let finalContentType: MediaType | null = contentType ?? null
        if (contentType !== null && contentType !== undefined) {
            let resolvedCharset = contentType.charset()
            if (resolvedCharset === null || resolvedCharset === undefined) {
                charset = Charset.UTF_8
                finalContentType = MediaType.toMediaTypeOrNull(`${contentType}; charset=utf-8`)
            } else {
                charset = resolvedCharset
            }
        }
        let textEncoder = util.TextEncoder.create(charset.name())
        let bytes = textEncoder.encodeInto(str) ?? new Uint8Array(0)
        return this.BytesToRequestBody(bytes, finalContentType, 0, bytes.byteLength)
    }

    /** Returns a new request body that transmits this. */
    // @JvmOverloads
    // @JvmStatic
    // @JvmName("create")
    static BytesToRequestBody(
        bytes: Uint8Array,
        contentType: MediaType | null = null,
        offset: number = 0,
        byteCount: number = bytes.byteLength
    ): RequestBody {
        checkOffsetAndCount(bytes.byteLength, offset, byteCount)
        
        class ByteArrayRequestBody extends RequestBody {
            override contentType(): MediaType | null {
                return contentType
            }
            
            override contentLength(): number {
                return byteCount
            }
            
            override async writeTo(sink: Sink): Promise<void> {
                await sink.write(bytes, byteCount)
            }
        }
        return new ByteArrayRequestBody()
    }


    /** Returns a new request body that transmits the content of this. */
    // @JvmStatic
    // @JvmName("create")
    static FileAsRequestBody(file: fs.File, contentType: MediaType | null = null): RequestBody {
        class FileRequestBody extends RequestBody {
            private fileSize: number
            
            constructor() {
                super()
                this.fileSize = fs.statSync(file.fd).size
            }
            
            override contentType(): MediaType | null {
                return contentType
            }

            override contentLength(): number {
                return this.fileSize
            }

            override async writeTo(sink: Sink): Promise<void> {
                const size = this.contentLength()
                const arrayBuffer = new ArrayBuffer(RequestBody.FILE_BUFFER_SIZE)
                let totalLength: number = 0
                let stream: fs.Stream | null = null
                
                try {
                    stream = fs.fdopenStreamSync(file.fd, 'r')
                    while (totalLength < size) {
                        const readSize: number = stream.readSync(arrayBuffer)
                        if (readSize <= 0) {
                            break
                        }
                        totalLength += readSize
                        await sink.write(new Uint8Array(arrayBuffer.slice(0, readSize)), readSize)
                    }
                } catch (err) {
                    const error = err as { message?: string; code?: string | number }
                    const errorMessage = `Failed to read file data: ${error.message || 'Unknown error'}, code: ${error.code || 'N/A'}`
                    console.error(errorMessage)
                    throw new IOException(errorMessage)
                } finally {
                    if (stream !== null && stream !== undefined) {
                        try {
                            stream.closeSync()
                        } catch (closeErr) {
                            console.error('Failed to close file stream:', closeErr)
                        }
                    }
                }
            }
        }
        return new FileRequestBody()
    }

}


